package com.stg.analytics.client;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Stack;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;

import com.stg.analytics.client.Event.MultiPartURL;

/**
 * @author colton
 * 
 *         Parses XML sent from the JavaScript client to the servlet. Adds the data to the database.
 * 
 */
public class SubmitServlet extends HttpServlet {

	// TODO, fix object oriented issues (they may cause concurrency issues with more multiple simultaneous requests);

	/**
	 * @author colton
	 * 
	 *         Helps in parsing the XML document specifically parses MultiPartURLs into the object as needed.
	 * 
	 */
	private static final class exMultiPartURL implements MultiPartURL {
		private URI host;
		private String parameters;
		private XMLEventReader reader;
		private boolean secure;
		private XMLEvent subEvent;
		private URI uri;

		/**
		 * @param reader
		 * 
		 * 
		 */
		@SuppressWarnings("deprecation")
		private exMultiPartURL(XMLEventReader reader) {
			this.reader = reader;
			nextChars();
			this.secure = Boolean.getBoolean(this.subEvent.asCharacters().getData());
			nextChars();
			try {
				this.host = new URI(this.subEvent.asCharacters().getData());
			} catch (URISyntaxException e) {
				throw new RuntimeException(e);
			}
			nextChars();
			try {
				this.uri = new URI(this.subEvent.asCharacters().getData());
			} catch (URISyntaxException e) {
				throw new RuntimeException(e);
			}
			try {
				nextChars();
				this.parameters = this.subEvent.asCharacters().getData();
				if (this.parameters != null && this.parameters.length() > 0) {
					this.parameters = URLDecoder.decode(this.parameters);
				}
			} catch (RuntimeException e) {
				if (e.getCause() instanceof Exception) {
					throw e;
				}
			}
		}

		@Override
		public URI getHost() {
			return this.host;
		}

		@Override
		public String getParameters() {
			return this.parameters;
		}

		@Override
		public URI getURI() {
			return this.uri;
		}

		@Override
		public boolean isSecure() {
			return this.secure;
		}

		private void nextChars() {
			do {
				try {
					this.subEvent = this.reader.nextEvent();
				} catch (XMLStreamException e) {
					throw new RuntimeException(e);
				}
				if (this.subEvent.isEndElement() && this.subEvent.asEndElement().getName().equals("HTMLSender")) {
					throw new RuntimeException(new Throwable("Past HTMLSender"));
				}
			} while (!this.subEvent.isCharacters());
		}
	}

	private Connection connection;

	/**
	 * 
	 */
	private static final long serialVersionUID = -4157314265192985705L;

	private Stack<Event> events = new Stack<Event>();

	XMLInputFactory xif = XMLInputFactory.newInstance();

	public SubmitServlet() throws ServletException {
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException cnfe) {
			throw new ServletException("Cannot find SQL driver", cnfe);
		}
		try {
			createStatement();
		} catch (SQLException e) {
			throw new ServletException("Could not connect to Database", e);
		}
	}

	/**
	 * Commits the data in the event stack to the database using a <code>PreparableStatement</code> to mitigate any SQL injection code.
	 * 
	 */
	private void commitData() {
		try {
			// Connection connection = DriverManager.getConnection("02t.vps.sparksis.com", "root", "iAmRoot");
			PreparedStatement statement = createStatement();
			for (Event e : this.events) {

				statement.setString(1, e.getType());
				statement.setString(2, e.getSession().getTokenID());
				statement.setBoolean(3, e.getHTMLSender().isSecure());
				statement.setString(4, e.getHTMLSender().getHost().toString());
				statement.setString(5, e.getHTMLSender().getURI().toString());
				statement.setString(6, e.getHTMLSender().getParameters());
				statement.setString(7, e.getPayload());
				statement.setTimestamp(8, new Timestamp(e.getTime().getTime()));
				statement.setString(9, e.getReference() == null ? null : e.getReference().toString());

				statement.execute();

			}
			statement.close();
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	private PreparedStatement createStatement() throws SQLException {
		String s = "CALL InsertEvent(?,?,?,?,?,?,?,?,?)";

		if (this.connection == null || this.connection.isClosed()) {
			this.connection = DriverManager.getConnection("jdbc:mysql://02t.vps.sparksis.com/Analytics", "root", "iAmRoot");
		}
		return this.connection.prepareStatement(s);
	}

	@Override
	public void destroy() {
		try {
			if (!this.connection.isClosed()) {
				this.connection.close();
			}
		} catch (SQLException e) {
			Logger.getLogger(this.getClass().getName()).warning("DB Connection did not close nicely; SQL State: " + e.getSQLState());
		}
		super.destroy();
	}

	/**
	 * 
	 * Sets the <code>Content-Length</code> to 0 and flushes the buffer prior to parsing the content to notify clients that they can send the next request or close the connection to
	 * free up additional resources
	 * 
	 * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setContentLength(0);
		resp.flushBuffer();

		String token = null;
		for (Cookie c : req.getCookies()) {
			if (c.getName().equals("tr_cookie")) {
				token = c.getValue();
				break;
			}
		}

		// File file = new File("aaa.xml");
		// file.setWritable(true);
		//
		// FileWriter out = new FileWriter(file);
		// BufferedReader in = new BufferedReader(new InputStreamReader(req.getInputStream()));
		// String line;
		// while((line = in.readLine())!=null){
		// out.write(line);
		// }
		// out.close();
		// in.close();

		try {
			parseDocument(req.getInputStream(), token);
		} catch (RuntimeException re) {
			throw new ServletException("Xml Parse failed", re);
		}

		commitData();

	}

	/**
	 * Look at the name of the method... Now doesn't it seem silly to ask what this does?
	 * 
	 * Okay so it does pull parsing of an XML document using the StAX API using the stream from the HTTP request.<br />
	 * Simple enough.
	 * 
	 * @param stream
	 *          A stream pointing to the xml document. Oh yeah this is servlet so this should point to the req.getInputStream().
	 * @param token
	 */
	@SuppressWarnings("deprecation")
	private void parseDocument(InputStream stream, final String token) {
		final XMLEventReader reader;
		try {
			reader = this.xif.createXMLEventReader(stream);
			Event event = null;
			while (reader.hasNext()) {
				final XMLEvent xmlEvent = reader.nextEvent();
				if (xmlEvent.isStartElement()) {
					if (xmlEvent.asStartElement().getName().getLocalPart().equals("Event")) {
						event = new Event();
						event.session = new Event.Session() {

							@Override
							public String getTokenID() {
								return token;
							}
						};
					} else if (event != null) {
						if (xmlEvent.asStartElement().getName().getLocalPart().equals("EventName")) {
							event.type = reader.nextEvent().asCharacters().getData();
						} else if (xmlEvent.asStartElement().getName().getLocalPart().equals("HTMLSender")) {
							event.HTMLSender = new exMultiPartURL(reader);
						} else if (xmlEvent.asStartElement().getName().getLocalPart().equals("Payload")) {
							event.payload = reader.nextEvent().asCharacters().getData();
							if (event.payload != null) {
								event.payload = URLDecoder.decode(event.payload);
							}
						} else if (xmlEvent.asStartElement().getName().getLocalPart().equals("Time")) {
							try {
								event.time = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(reader.nextEvent().asCharacters().getData());
							} catch (ParseException e) {
								throw new RuntimeException(e);
							}
						} else if (xmlEvent.asStartElement().getName().getLocalPart().equals("Reference")) {
							event.reference = new exMultiPartURL(reader);
						}
					}
				} else if (xmlEvent.isEndElement() && xmlEvent.asEndElement().getName().getLocalPart().equals("Event")) {
					this.events.add(event);
					event = null;
				}
			}
		} catch (XMLStreamException e) {
			throw new RuntimeException(e);
		}
	}
}
